﻿namespace GreenIsomer.Web.Controls.JQuery {
	using System;
	using System.Web.UI;
	using System.Collections.Generic;
	using System.Web.UI.WebControls;
	using System.ComponentModel;
	using System.Globalization;

	/// <summary>
	/// Server control that implements the jQuery UI Sortable interaction to allow a list of items to be custom sorted by the user.
	/// </summary>
	[ParseChildren(true, "Items")]
	public class SortableList : JQueryWebControl {


		#region Constants: Property defaults

		private const int defaultDistance = 1;
		private const int defaultDelay = 0;

		#endregion


		#region Static Methods: Private routines

		private static void CheckIndexes(int[] indexes, out bool sequential, out bool valid) {

			int length = indexes.Length;
			List<int> checkedIndexes = new List<int>(indexes.Length);

			sequential = true;

			for (int i = 0; i < length; i++) {
				int index = indexes[i];

				if(i != index) sequential = false;

				if(index < 0 || index >= length || checkedIndexes.Contains(index)) {
					valid = false;
					return;
				}

				checkedIndexes.Add(index);
			}

			valid = true;
		}

		#endregion


		#region Members

		private ListItemCollection items;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the GreenIsomer.Web.Controls.JQuery.SortableList class.
		/// </summary>
		public SortableList() : base(HtmlTextWriterTag.Ul) { }

		#endregion


		#region Properties: Widget initialization options

		/// <summary>
		/// Time in milliseconds to define when the sorting should start. It helps preventing unwanted drags when clicking on an element.
		/// This property maps directly to the "delay" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultDelay), Category("Behavior")]
		public virtual int Delay {
			get { return ViewState.GetValue("Delay", defaultDelay); }
			set { ViewState["Delay"] = value; }
		}

		/// <summary>
		/// Tolerance, in pixels, for when sorting should start. If specified, sorting will not start until after mouse is dragged beyond distance. Can be used to allow for clicks on elements within a handle.
		/// This property maps directly to the "distance" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultDistance), Category("Behavior")]
		public virtual int Distance {
			get { return ViewState.GetValue("Distance", defaultDistance); }
			set { ViewState["Distance"] = value; }
		}

		#endregion


		#region Properties: Additional

		/// <summary>
		/// Gets the collection of list items in the list control.
		/// </summary>
		[DefaultValue(null), MergableProperty(false), PersistenceMode(PersistenceMode.InnerDefaultProperty), Category("Misc")]
		public virtual ListItemCollection Items {
			get {
				if(items == null) {
					items = new ListItemCollection();
					if(IsTrackingViewState) ((IStateManager)items).TrackViewState();
				}
				return items;
			}
		}

		#endregion


		#region Properties: Subcontrol

		/// <summary>
		/// Gets or sets the hidden field used to receive an updated sort order.
		/// </summary>
		protected virtual PostBackField SortOrderField { get; set; }

		/// <summary>
		/// Gets the rendered element ID of the hidden field used to receive an updated sort order.
		/// </summary>
		public virtual string SortOrderClientId {
			get { return SortOrderField.ClientID; }
		}

		#endregion


		#region Events

		/// <summary>
		/// Occurs on postback when the sort order of the Items collection was changed on the client.
		/// </summary>
		[Category("Action")]
		public event EventHandler SortOrderChanged;

		#endregion


		#region Methods: Event hooks

		/// <summary>
		/// Raises the SortOrderChanged event.
		/// </summary>
		/// <param name="e">The System.EventArgs object that contains the event data.</param>
		protected virtual void OnSortOrderChanged(EventArgs e) {
			if(SortOrderChanged != null) SortOrderChanged.Invoke(this, e);
		}

		#endregion


		#region Methods: View State

		/// <summary>
		/// Causes the control to track changes to its view state so they can be stored in the object's System.Web.UI.Control.ViewState property.
		/// This method is used primarily by control developers.
		/// </summary>
		protected override void TrackViewState() {
			base.TrackViewState();
			IStateManager manager = items as IStateManager;
			if(manager != null) manager.TrackViewState();
		}

		/// <summary>
		/// Saves any state that was modified after the TrackViewState() method was invoked.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <returns>An object that represents the control state to round-trip to the client.</returns>
		protected override object SaveViewState() {

			object baseState = base.SaveViewState();

			object itemsState = null;
			IStateManager manager = items as IStateManager;
			if(manager != null) itemsState = manager.SaveViewState();

			if(baseState == null && itemsState == null) return null;

			List<int> selectedState = new List<int>();
			for(int i = 0; i < items.Count; i++)
				if(items[i].Selected) selectedState.Add(i);

			return new Triplet(baseState, itemsState, selectedState.Count == 0 ? null : selectedState.ToArray());
		}

		/// <summary>
		/// Restores view-state information from a previous request that was saved with the SaveViewState() method.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="savedState">An object that represents the control state to restore.</param>
		protected override void LoadViewState(object savedState) {
			object baseState = null;
			object itemsState = null;
			int[] selectedState = null;

			Triplet triplet = savedState as Triplet;
			if(triplet != null) {
				baseState = triplet.First;
				itemsState = triplet.Second;
				selectedState = triplet.Third as int[];
			}

			base.LoadViewState(baseState);

			if(itemsState != null) {
				IStateManager manager = Items as IStateManager;
				manager.LoadViewState(itemsState);
			}

			//TODO: Decide if Selected Items should be stored in viewstate independently.  If so, use pair
			if(selectedState != null)
				foreach(int index in selectedState)
					if(index >= 0 && index < items.Count)
						items[index].Selected = true;
		}

		#endregion


		#region Methods: Rendering hooks

		/// <summary>
		/// Allows a jQuery server control to reference scripts and stylesheets necessary to render their client markup and widgets.
		/// </summary>
		/// <param name="manager">The GreenIsomer.Web.Controls.JQuery.Manager used to handle style and script referencing in addition to unified generation of load and ready scripts.</param>
		protected override void Register(Manager manager) {

			manager.RegisterScriptFile(Settings.Default.CoreScriptFile);
			manager.RegisterScriptFile(Settings.Default.UiCoreScriptFile);
			manager.RegisterScriptFile(Settings.Default.UiSortableScriptFile);

			manager.RegisterStyleFile(Settings.Default.UiCoreStyleFile);
			manager.RegisterThemeStyleFile();

			manager.RegisterControl(this);

			EnsureChildControls();
		}

		/// <summary>
		/// Sends server control content to a provided System.Web.UI.HtmlTextWriter object, which writes the content to be rendered to the client.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A System.Web.UI.HtmlTextWriter that represents the output stream to render HTML content on the client.</param>
		protected override void Render(HtmlTextWriter writer) {
			if(!MasterUseCssStyling) {
				writer.AddStyleAttribute(HtmlTextWriterStyle.ListStyleType, "none");
				writer.AddStyleAttribute(HtmlTextWriterStyle.Padding, "0");
			}
			base.Render(writer);
		}

		/// <summary>
		/// Renders the contents of the control to the specified writer.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A System.Web.UI.HtmlTextWriter that represents the output stream to render HTML content on the client.</param>
		protected override void RenderContents(HtmlTextWriter writer) {
			base.RenderContents(writer);

			bool useStyleAttributes = !MasterUseCssStyling;
			string idBase = ClientID + ClientIDSeparator;

			for(int i = 0; i < items.Count; i++) {
				ListItem item = items[i];

				string cssClass = "ui-state-default";
				if(!item.Enabled) cssClass += " ui-state-disabled";
				if(item.Selected) cssClass += " ui-state-highlight";

				writer.AddAttribute(HtmlTextWriterAttribute.Class, cssClass);
				writer.AddAttribute(HtmlTextWriterAttribute.Id, idBase + i.ToString(CultureInfo.InvariantCulture));
				if(useStyleAttributes)
					writer.AddStyleAttribute(HtmlTextWriterStyle.PaddingLeft, "1.5em");
				writer.RenderBeginTag(HtmlTextWriterTag.Li);

				if(item.Enabled) {
					writer.AddAttribute(HtmlTextWriterAttribute.Class, "ui-icon ui-icon-arrowthick-2-n-s");
					if(useStyleAttributes) {
						writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "absolute");
						writer.AddStyleAttribute(HtmlTextWriterStyle.MarginLeft, "-1.3em");
					}
					writer.RenderBeginTag(HtmlTextWriterTag.Span);
					writer.RenderEndTag();
				}

				writer.WriteEncodedText(item.Text);

				writer.RenderEndTag();
			}
		}

		/// <summary>
		/// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparatin for posting back or rendering.
		/// This method is used primarily by control developers.
		/// </summary>
		protected override void CreateChildControls() {
			base.CreateChildControls();

			//TODO: The naming has to be resolved (maybe)

			string idBase = ID + ClientIDSeparator;

			SortOrderField = new PostBackField();
			SortOrderField.ID = idBase + "SortOrder";
			SortOrderField.ValueChanged += new System.EventHandler<PostBackFieldEventArgs>(SortOrderField_ValueChanged);
			Controls.Add(SortOrderField);
		}

		/// <summary>
		/// Renders the load script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A GreenIsomer.Web.Controls.JQuery.ScriptBuilder that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderLoadScript(ScriptBuilder script) {

			script.RenderSelector(ClientID);

			script.Append(".sortable(");
			script.RenderProperties(CreateProperties());
			script.Append(");");

			script.NewLine();

			script.RenderSelector(ClientID);
			script.Append(".disableSelection();");

			//TODO: Hover Code is Buggy on Client for some reason
			//script.NewLine();

			//script.RenderSelector(ClientID, " li:not(.ui-state-disabled)");
			//script.Append(".hover(function(){$(this).addClass('ui-state-hover');},function(){$(this).removeClass('ui-state-hover');});");

		}

		/// <summary>
		/// Creates the properties object that will be rendered as the jQuery UI initialization options.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <returns>A GreenIsomer.Web.Controls.JQuery.Properties object that will become the jQuery UI initialization options.</returns>
		protected override PropertyCollection CreateProperties() {
			PropertyCollection properties = base.CreateProperties();

			properties.AddValue("axis", "y");

			properties.AddValue("cancel", ".ui-state-disabled");

			properties.AddValue("cursor", "row-resize");

			int delay = Delay;
			if(delay != defaultDelay) properties.AddValue("delay", delay);

			int distance = Distance;
			if(distance != defaultDistance) properties.AddValue("distance", distance);

			ScriptBuilder updateScript = new ScriptBuilder();
			RenderUpdateScript(updateScript);
			if(updateScript.Length != 0) properties.AddScript("update", updateScript);

			return properties;
		}

		/// <summary>
		/// Renders the client "update" event script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A GreenIsomer.Web.Controls.JQuery.ScriptBuilder that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderUpdateScript(ScriptBuilder script) {

			int startPosition = ClientID.Length + 1;

			script.Append("function(event,ui){");

			script.Append("var result = '';");

			script.RenderSelector(ClientID, " li");
			script.Append(".each(function(index){if(index != 0){result += ','};result += $(this).attr('id').substring(");
			script.AppendValue(startPosition);
			script.Append(");});");

			script.RenderSelector(SortOrderClientId);
			script.Append(".val(result);");

			script.Append("}");
		}

		#endregion


		#region Event Handlers

		private void SortOrderField_ValueChanged(object sender, PostBackFieldEventArgs e) {

			string sortOrder = SortOrderField.Value;
			if(string.IsNullOrEmpty(sortOrder)) return;

			int[] indexes;
			try {
				indexes = Array.ConvertAll(sortOrder.Split(','), new Converter<string, int>(Convert.ToInt32));
			} catch(FormatException) {
				return;
			} catch(OverflowException) {
				return;
			}
			//TODO: Decide if exception should be thrown or error ignored in these cases....

			ListItemCollection oldItems = Items;
			if(indexes.Length != oldItems.Count) return;

			bool sequential;
			bool valid;
			CheckIndexes(indexes, out sequential, out valid);

			//TODO: If it is definitively decided that errors are ignored, then the distinction doesn't matter
			if(sequential || !valid) return;

			ListItem[] newItems = new ListItem[indexes.Length];
			for(int i = 0; i < indexes.Length; i++)
				newItems[i] = oldItems[indexes[i]];

			items.Clear();
			items.AddRange(newItems);

			SortOrderField.Value = string.Empty;

			OnSortOrderChanged(EventArgs.Empty);
		}

		#endregion


	}

}